package com.hexus.eve.entities.security;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import com.hexus.eve.application.BeanFactory;
import com.hexus.eve.application.Constants;
import com.hexus.eve.utils.EmptyItems;

/**
 * This class is used for encrypting and decrypting values basing on a compound key (salt).
 * Compound key is a mix of global and local key.
 * Any user-specific unique string can be used as local key.
 * Global key is defined as corresponding property on server side.
 * 
 * @author Mikhail_Berastau
 *
 */
@SuppressWarnings("restriction")
public class CipherEncrypter {
	private Cipher cipher;
	private SecretKeySpec key;
	
	
	/**
	 * Constructor with default global key (only local key is defined by a caller)
	 * @param localKey user-specific unique string
	 */
	public CipherEncrypter(String localKey) {
		Constants constants = (Constants) BeanFactory.getBean(Constants.class);
		//By default global key is a property on server side
		String globalKey = constants.getProperty(Constants.PropertyNames.GLOBAL_KEY);
		initialize(localKey, globalKey);
	}
	
	/**
	 * Constructor which allows defining both global and local key
	 * @param localKey user-specific unique string
	 * @param globalKey defined in application (one-for-all key) 
	 */
	public CipherEncrypter(String localKey, String globalKey) {
		initialize(localKey, globalKey);
	}
	
	/**
	 * Initializes cipher according to local and global keys
	 * @param localKey user-unique value to make similar data differ when ciphered (open part of key)
	 * @param globalKey application key which has nothing to do with user data (secure part of key)
	 */
	private void initialize(String localKey, String globalKey){
		byte[] gKey = EmptyItems.BYTE_ARRAY;
		byte[] lKey = EmptyItems.BYTE_ARRAY;
		try {
			gKey = globalKey.getBytes("UTF-8");
			lKey = localKey.getBytes("UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		
		// Mixing local and global key
		byte[] keyBytes = new byte[16];
		int gkIndex = 0;
		int lkIndex = 0;
		for(int i = 0; i < 8; i++) {
			keyBytes[i * 2] = gKey[gkIndex++];
			keyBytes[i * 2 + 1] = (lkIndex < lKey.length) ? lKey[lkIndex++] : gKey[gkIndex++];
		}
		
		key = new SecretKeySpec(keyBytes, "AES");
		
		try {
			cipher = Cipher.getInstance("AES/ECB/NoPadding");
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Encrypts a value
	 * @param value value to be encrypted
	 * @return cipher - an encrypted version of the input value
	 */
	public String encrypt(String value) {
		byte[] input = value.getBytes();
		try {
			cipher.init(Cipher.ENCRYPT_MODE, key);
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		}
		
		byte[] result = new byte[cipher.getOutputSize(input.length)];
		try {
			result = cipher.doFinal(input);
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		}
		
		return new BASE64Encoder().encode(result);
	}
	
	/**
	 * Decrypts a value
	 * @param value cipher to be encrypted
	 * @return decrypted value
	 */	
	public String decrypt(String value){
		try {
			cipher.init(Cipher.DECRYPT_MODE, key);
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		}
		BASE64Decoder decoder = new BASE64Decoder();
		byte[] result = EmptyItems.BYTE_ARRAY;
		try {
			result = cipher.doFinal(decoder.decodeBuffer(value));
		} catch (IllegalBlockSizeException e) {
			e.printStackTrace();
		} catch (BadPaddingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return new String(result);
	}
	
}
